﻿// Author: Ben
// Project: CR_RandomBackgroundChanger
// Path: D:\code\Abi\CR_RandomBackgroundChanger
// Creation date: 26.04.2008 15:09
// Last modified: 02.06.2009 11:17

#region Using directives
using DevExpress.CodeRush.Core;
using EnvDTE;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.IO;
#endregion

namespace CR_RandomBackgroundChanger
{
	public class ActiveViewBackgroundHelper : NativeWindow, IDisposable
	{
		#region Constants
		/// <summary>
		/// Filename used to cache image filenames, which is faster to load
		/// than going through all the directory at every startup!
		/// Update: Using the temp directory now, it is always the same and
		/// we do not really care if someone deletes this file there or the
		/// OS cleans up, we can always regenerate this file!
		/// </summary>
		static string ImageFilenamesCacheFilename
		{
			get
			{
				return Path.Combine(Path.GetTempPath(), "ImageFilenames.txt");
			}
		} // ImageFilenamesCacheFilename
		#endregion

		#region Variables
		/// <summary>
		/// These bitmap variables are initialized in DrawBackgroundImage and will
		/// stay the same as long as the size of them does not change.
		/// They will be released in the Dispose method. Make sure you call it!
		/// </summary>
		Bitmap sourceImage = null,
			targetBitmap = null;

		/// <summary>
		/// Helper flag to make sure we do NOT call DrawBackgroundImage again
		/// once we started executing it. This flag will be set to false after
		/// executing DrawBackgroundImage, but this way we can have other WM_PAINT
		/// events, that will just paint VS text and stuff and not interfer with
		/// our background painting (if we don't do this more flickering happens).
		/// </summary>
		bool drawingBackground = false;

		/// <summary>
		/// Remember background color for options screen, to make preview look
		/// the same as the code editor. Also used inside DrawBackgroundImage,
		/// but not anywhere else!
		/// </summary>
		internal static Color backgroundColorPixel = Color.White;

		// Static stuff for GetBackgroundImage (mostly caching)
		static Dictionary<int, Image> currentBackgroundImagePerWindow =
			new Dictionary<int, Image>();
		static float lastTransparency = 0.0f;
		static List<string> cachedImageFilenames = null;
		static string lastRandomFilenameForOptions = null;
		static DateTime lastTimeChangedBackground = DateTime.Now;
		#endregion

		#region Constructor
		/// <summary>
		/// Create active view background helper class, each document view
		/// will get an instance of this class to handle the background drawing!
		/// </summary>
		public ActiveViewBackgroundHelper(IntPtr windowHandle)
		{
			base.AssignHandle(windowHandle);
		} // ActiveViewBackgroundHelper(windowHandle)
		#endregion

		#region GetBackgroundImage
		/// <summary>
		/// Clear background image, used for updating previews in Options dialog
		/// mostly.
		/// </summary>
		public static void ClearBackgroundImage(bool forceRemovingFilenameCache,
			bool clearRandomFilename)
		{
			// This is enough to force an update
			lastTransparency = 0.0f;

			if (clearRandomFilename)
			{
				lastRandomFilenameForOptions = null;
				// Kill cached images, force reload for them all ;)
				currentBackgroundImagePerWindow.Clear();
			}

			// Also kill cached image filenames to force reload if specified
			if (forceRemovingFilenameCache)
			{
				cachedImageFilenames = null;
				// Kill cached images, force reload for them all ;)
				currentBackgroundImagePerWindow.Clear();
				// Also kill cached file if one exists yet
				if (File.Exists(ImageFilenamesCacheFilename))
					File.Delete(ImageFilenamesCacheFilename);
			} // if
		} // ClearBackgroundImage()

		/// <summary>
		/// Recursively add image filenames, this can take a while if we are
		/// searching for a lot of files. Tested up to 100k files, with caching
		/// thanks to ImageFilenamesCacheFilename loading is still pretty good,
		/// only the first time searching for all the files can take a few secs.
		/// </summary>
		/// <returns>Number of image filenames added here</returns>
		private static int RecursivelyAddImageFilenames(string directory)
		{
			int filenamesAdded = 0;

			// Add all files that look like bitmaps (.jpg, .jpeg, .bmp, .png, .tga)
			string[] files = Directory.GetFiles(directory, "*.jpg",
				SearchOption.AllDirectories);//*.jpeg;*.bmp;*.png;*.tga");
			foreach (string file in files)
				//too slow: if (StringHelper.IsInList(StringHelper.GetExtension(file),
				//	new string[] { "jpg", "jpeg", "bmp", "png", "tga" }, true))
				{
					cachedImageFilenames.Add(file);
					filenamesAdded++;
				} // foreach if

			// Handle subdirectories
			/*already done above with SearchOption
			string[] subDirectories = Directory.GetDirectories(directory);
			foreach (string subDir in subDirectories)
				filenamesAdded += RecursivelyAddImageFilenames(subDir);
			 */

			// Return number of filenames we added
			return filenamesAdded;
		} // RecursivelyAddImageFilenames(directory)

		/// <summary>
		/// Get the current background image we currently wanna use, can be null
		/// if no background images directory is specified. This method will
		/// also return different results every x minutes (see settings, usually
		/// every 5 minutes) or even different results depending on which
		/// document is open! A lot of caching and optimization goes on here,
		/// so this method might look a little confusing, but good performance
		/// is more important than anything else for this addin (else it would
		/// just be annoying and not useful).
		/// </summary>
		public static Image GetBackgroundImage(int windowHandle,
			Color backgroundColor, float transparency)
		{
			// Not enabled? Then don't paint any background images!
			if (Options.enabled == false)
				return null;

			// Search for image filenames if we have not done that yet.
			// To refresh these a VS restart is required!
			if (cachedImageFilenames == null &&
				Options.backgroundImageDirectory.Length > 0)
			{
				cachedImageFilenames = new List<string>();
			
				// Do we have a cache file for this already?
				try
				{
					if (File.Exists(ImageFilenamesCacheFilename))
					{
						FileStream file = new FileStream(
							ImageFilenamesCacheFilename, FileMode.Open,
							FileAccess.Read, FileShare.ReadWrite);
						StreamReader reader = new StreamReader(file,
							System.Text.Encoding.UTF8);
						while (reader.Peek() >= 0)
						{
							cachedImageFilenames.Add(reader.ReadLine());
						} // while
						file.Close();
					}
				}
				catch (Exception e)
				{
					Log.Write("Failed loading " + ImageFilenamesCacheFilename +
						": " + e.ToString());
				} // catch (e)

				try
				{
					// If loading did not succeed, go ahead searching for files!
					if (cachedImageFilenames.Count == 0)
					{
						RecursivelyAddImageFilenames(Options.backgroundImageDirectory);

						// Create Image filename cache helper file
						FileStream file = new FileStream(
							ImageFilenamesCacheFilename, FileMode.Create,
							FileAccess.Write, FileShare.ReadWrite);

						StreamWriter writer = new StreamWriter(file,
							System.Text.Encoding.UTF8);
						foreach (string filename in cachedImageFilenames)
							writer.WriteLine(filename);
						writer.Flush();
						file.Close();
					} // if
				} // try
				catch (Exception ex)
				{
					Log.Write("Failed to add background images: " + ex.ToString());
				}
			} // if

			// Got no images to process? Then we gotta quit!
			if (cachedImageFilenames == null ||
				cachedImageFilenames.Count == 0)
				return null;

			// Use different backgrounds for different windows if specified,
			// else always use the same (by setting window handle to 0)!
			if (Options.useDifferentImagesForDifferentDocuments == false)
				windowHandle = 0;

			// Do we want to change the background image every x minutes?
			if (Options.changeImageEveryFewMinutes &&
				// And did we reach the given timeout in minutes?
				lastTimeChangedBackground.AddMinutes(Options.minutesToChangeImage) <
				DateTime.Now)
			{
				// Then just force a full update, clear all the cache
				lastTransparency = 0.0f;
				currentBackgroundImagePerWindow.Clear();
			} // if

			if (currentBackgroundImagePerWindow.ContainsKey(windowHandle) == false ||
				lastTransparency != transparency)
			{
				if (lastTransparency != transparency)
				{
					lastTransparency = transparency;
					lastTimeChangedBackground = DateTime.Now;
				}

				// Try to load a random background image.
				// But keep the current image in case we just updating (options screen)
				string randomFilename = "";
				if (windowHandle == 0 &&
					lastRandomFilenameForOptions != null &&
					currentBackgroundImagePerWindow.ContainsKey(windowHandle))
					randomFilename = lastRandomFilenameForOptions;
				else
				{
					randomFilename = cachedImageFilenames[
						RandomHelper.GetRandomInt(cachedImageFilenames.Count)];
					if (windowHandle == 0)
						lastRandomFilenameForOptions = randomFilename;
				} // else

				try
				{
					// Try to load image, will fail if image does not exist anymore
					// or anything is wrong with it.
					Image backgroundImage = Bitmap.FromFile(randomFilename);

					// Make image transparent by painting the background color on
					// top of it (same effect as drawing the image with transparecny
					// on top, but that is WAY slower!)
					// Note: This might fail too because graphics does not like some
					// images (e.g. indexed bitmaps). Just fail and remove it ..
					Graphics helperGraphics = Graphics.FromImage(backgroundImage);

					// Paint backgroundColor on top with given transparency
					if (transparency < 1.0f)
						helperGraphics.FillRectangle(new SolidBrush(
							// Create new color from given transparency setting
							Color.FromArgb((byte)(255 * (1.0f - transparency)),
							backgroundColor)), new Rectangle(
							0, 0, backgroundImage.Width, backgroundImage.Height));

					// Dispose helperGraphics, we don't need it anymore
					helperGraphics.Dispose();

					// Replace image if key is already existant in the dictionary
					// (used for refreshing in Options screen)
					if (currentBackgroundImagePerWindow.ContainsKey(windowHandle))
						currentBackgroundImagePerWindow[windowHandle] = backgroundImage;
					else
						// Or add this to the cached dictionary (normal case)
						currentBackgroundImagePerWindow.Add(windowHandle, backgroundImage);
				} // catch
#if DEBUG
				catch (Exception ex)
				{
					Log.Write("Failed to load random background image " + randomFilename +
						": " + ex.ToString());
#else
				catch
				{
#endif
					// If this fails, remember that and do not try again!
					cachedImageFilenames.Remove(randomFilename);
					if (windowHandle == 0)
						lastRandomFilenameForOptions = null;
					// This try failed, go at it again until we got it right or
					// no more working files ..
					return null;
				} // catch
			} // if

			// Return the cached result
			return currentBackgroundImagePerWindow[windowHandle];
		} // BackgroundImage
		#endregion

		#region DrawBackgroundImage
		/// <summary>
		/// This method draws the background and is called from WndProc whenever
		/// it intercepts a WM_PAINT message. Again, some caching and confusing
		/// optimized code is in here too, again for getting good performance.
		/// </summary>
		private void DrawBackgroundImage(IntPtr activeWindowHandle,
			Rectangle rect)
		{
			// Create an image for storing the orginal editor screen. Only do this if
			// we have to, no need to create if nothing has changed since last time!
			try
			{
				if (sourceImage == null ||
					sourceImage.Width != rect.Width ||
					sourceImage.Height != rect.Height)
				{
					if (sourceImage != null)
						sourceImage.Dispose();
					sourceImage = new Bitmap(rect.Width, rect.Height);
				} // if
			} // try
			catch
			{
				// This can fail when sourceImage is disposed or currently being
				// disposed (then .Width will fail). Just create a new sourceImage
				// in that case!
				sourceImage = new Bitmap(rect.Width, rect.Height);
			} // catch

			// Always create new graphics object, else we won't have current data
			Graphics sourceImageGraphics = Graphics.FromImage(sourceImage);

			// And grab current editor window content and copy it to it!
			IntPtr hdc = sourceImageGraphics.GetHdc();
			Win32.PrintWindow(activeWindowHandle, hdc, 1);
			sourceImageGraphics.ReleaseHdc(hdc);

			// Next find the color on the bottom right and use it as the
			// transparent color! Note: Antialasing will cause artifacts, make sure
			// the backgroundImage fits to the background color, e.g. by using a
			// lot of alpha transparency (<25% visibility)!
			// Update 2009-06-12: Changed pixel location for getting background
			// color, sometimes popups in the lower right interfered with the color!
			backgroundColorPixel =
				sourceImage.GetPixel(rect.Width - 1, 0);//obs: rect.Height - 1);
			sourceImage.MakeTransparent(backgroundColorPixel);

			// Create target image where we wanna paint to, this is important
			// because drawing directly to the VS window will produce flickering!
			try
			{
				if (targetBitmap == null ||
					targetBitmap.Width != rect.Width ||
					targetBitmap.Height != rect.Height)
				{
					if (targetBitmap != null)
						targetBitmap.Dispose();

					targetBitmap = new Bitmap(rect.Width, rect.Height);
				} // if
			} // try
			catch
			{
				// This can fail when sourceImage is disposed or currently being
				// disposed (then .Width will fail). Just create a new sourceImage
				// in that case!
				targetBitmap = new Bitmap(rect.Width, rect.Height);
			} // catch
			Graphics targetBitmapGraphics = Graphics.FromImage(targetBitmap);

			// Clear background with pixel color
			//not required, we draw solid image:
			//targetBitmapGraphics.Clear(backgroundColorPixel);

			// Draw background image (tiled and transparent if specified)
			float transparency = Options.transparency / 100.0f;
			Image backgroundImage = GetBackgroundImage((int)activeWindowHandle,
				backgroundColorPixel, transparency);
			
			// Do the drawing as many times as we need to tile to fill everything!
			for (int y = 0; y < rect.Height; y+=backgroundImage.Height)
				for (int x = 0; x < rect.Width; x += backgroundImage.Width)
				{
					Rectangle destRect = new Rectangle(
						x, y, backgroundImage.Width, backgroundImage.Height);
					Rectangle srcRect = new Rectangle(
						0, 0, backgroundImage.Width, backgroundImage.Height);
					// Reaching screen border, then reduce rect. DrawImage will clip
					// this anyway, but maybe this improves performance a bit!
					if (x + backgroundImage.Width > rect.Width)
					{
						int subWidth = x + backgroundImage.Width - rect.Width;
						destRect.Width -= subWidth;
						srcRect.Width -= subWidth;
					} // if
					if (y + backgroundImage.Height > rect.Height)
					{
						int subHeight = y + backgroundImage.Height - rect.Height;
						destRect.Height -= subHeight;
						srcRect.Height -= subHeight;
					} // if

					targetBitmapGraphics.DrawImage(backgroundImage, destRect, srcRect,
						GraphicsUnit.Pixel);
				} // for for

			/*This also works for displaying a transparent background image,
			 * but this is just too slow. It makes everything 3-4 times slower.
			 * Instead we precalculate all this transparency in the
			 * GetBackgroundImage method directly!
			
			// Build transparent image attributes
			float transparency = 0.25f;
			float[][] transparentMatrixArray =
			{
				new float[] {1, 0, 0, 0, 0},
				new float[] {0, 1, 0, 0, 0},
				new float[] {0, 0, 1, 0, 0},
				new float[] {0, 0, 0, transparency, 0},
				new float[] {0, 0, 0, 0, 1}
			};
			ImageAttributes transparentImageAttributes = new ImageAttributes();
			transparentImageAttributes.SetColorMatrix(
				new ColorMatrix(transparentMatrixArray),
				ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

			//TODO: as many times as we need to tile!
			for (int y = 0; y < rect.Height; y+=backgroundImage.Height)
				for (int x = 0; x < rect.Width; x += backgroundImage.Width)
				{
					//solid:
					//targetBitmapGraphics.DrawImage(backgroundImage, x, y);
					//tiled: see above
					//transparent: slow!
					// Transparent:
					targetBitmapGraphics.DrawImage(backgroundImage, new Rectangle(
						x, y, backgroundImage.Width, backgroundImage.Height),
						0, 0, backgroundImage.Width, backgroundImage.Height,
						GraphicsUnit.Pixel, transparentImageAttributes);
				} // for for
			 */

			// Now draw source image on top (text and foreground stuff), else we
			// would only see our distracting background image and while that is
			// fun, we sometimes still need to be productive and see the editor
			// text ^^
			targetBitmapGraphics.DrawImage(sourceImage, 0, 0);

			// Finally draw on the VS window, but only do one single draw here
			// to make sure we do not have any flickering!
			Graphics graphics = Graphics.FromHwnd(activeWindowHandle);
			graphics.DrawImage(targetBitmap, 0, 0);

			// This is important: Validate the rect so all this can now be
			// displayed! All WM_PAINT calls during this method will
			// be handled normally (without our background painting), which is
			// used for normal updates because we mess everything up ^^
			Win32.ValidateRect(activeWindowHandle, ref rect);

			// Dispose everything we do not need anymore
			// Note: We always have to create new graphics and dispose them here,
			// else updating sourceImage, targetBitmap, etc. does not work!
			if (graphics != null)
				graphics.Dispose();
			if (sourceImageGraphics != null)
				sourceImageGraphics.Dispose();
			if (targetBitmapGraphics != null)
				targetBitmapGraphics.Dispose();
		} // DrawBackgroundImage(activeWindowHandle, rect)
		#endregion

		#region WndProc
		/// <summary>
		/// WndProc, we are only interested at the WM_PAINT event!
		/// If a paint happens and we got a valid  we will call DrawBackgroundImage
		/// </summary>
		/// <param name="m"></param>
		protected override void WndProc(ref Message m)
		{
			// Just make sure we mark the erase background message as handled.
			// Will not do anything anyway (all painting done in WM_PAINT).
			if (m.Msg == (int)WindowsMessages.WM_ERASEBKGND)
			{
				//Log.Write("WndProc " + Win32.GetWindowMessageName(m.Msg));
				// Mark event is already handled
				m.Result = (IntPtr)1;
			} // if

			// Handle the paint event
			if (m.Msg == (int)WindowsMessages.WM_PAINT &&
				// Are we currently drawing our own background? Then make sure we do
				// not handle this and use the default message handling instead!
				drawingBackground == false &&
				// Make sure we got a view, else we don't have a windows handle!
				CodeRush.TextViews.Active != null &&
				// Check for invalid hwnd, then we can't paint backgroundImage!
				(int)CodeRush.TextViews.Active.Handle != 0)
			{
				//Log.Write("WndProc " + Win32.GetWindowMessageName(m.Msg));

				TextView view = CodeRush.TextViews.Active;
				IntPtr activeWindowHandle = view.Handle;
				Rectangle rect = new Rectangle();
				Win32.GetUpdateRect(activeWindowHandle, ref rect, false);
				// Only proceed if we have a valid rect
				if (Win32.IsRectEmpty(ref rect) == false)
				{
					// Make sure we mark this flag so subsequent calls to WM_PAINT
					// will actually just paint the normal stuff, not just our
					// background rendering!
					drawingBackground = true;
					DrawBackgroundImage(activeWindowHandle, rect);
					drawingBackground = false;

					// Mark event is already handled
					m.Result = (IntPtr)1;

					// Do not call base.WndProc, we don't want to process it here!
					// Instead we invalidate inside DrawBackgroundImage and force
					// a new WM_PAINT event inside there, which will be executed
					// because drawingBackground is still true while in there.
					return;
				} // if (rect)
			} // if (WM_PAINT)

			base.WndProc(ref m);
		} // WndProc()
		#endregion

		#region IDisposable Members
		public void Dispose()
		{
			// Dispose source and target images, everything else is disposed
			// automatically. Note: Do not dispose any static variables, they
			// are shared between all ActiveViewBackgroundHelper instances.
			if (sourceImage != null)
				sourceImage.Dispose();
			if (targetBitmap != null)
				targetBitmap.Dispose();
		} // Dispose()
		#endregion
	} // class ActiveViewBackgroundHelper
} // namespace CR_RandomBackgroundChanger
